# import copy
from datetime import datetime
from PyQt5.QtCore import Qt, QAbstractTableModel, QModelIndex
from PyQt5.QtWidgets import QTableView, QMenu, QInputDialog, QErrorMessage, QDialog, QDialogButtonBox, QVBoxLayout
import traceback
import logging
from PyQt5 import QtCore,QtWidgets
from MyTableView import MyTableView
def getAttrRecursive(obj, attr):
    """ Recursive introspection (i.e. get the member 'b' of a member 'a' by name as 'a.b').
    """
    return getattr(obj, attr)
    # try:
    #     p = attr.index(".")
    #     obj = getattr(obj, attr[0:p])
    #     return getAttrRecursive(obj, attr[p+1:])
    # except ValueError:
    #     return getattr(obj, attr)


def setAttrRecursive(obj, attr, value):
    """ Recursive introspection (i.e. set the member 'b' of a member 'a' by name as 'a.b').
    """
    setattr(obj, attr, value)
    # try:
    #     p = attr.index(".")
    #     obj = getattr(obj, attr[0:p])
    #     setAttrRecursive(obj, attr[p+1:], value)
    # except ValueError:
    #     setattr(obj, attr, value)


class ObjectModel(QAbstractTableModel):
    def __init__(self,  parent=None):
        QAbstractTableModel.__init__(self, parent)
        self.objects = []
        self.properties =  []
    def getObject(self, index):
        if not index.isValid():
            return None
        objectIndex = index.row()
        try:
            return self.objects[objectIndex]
        except IndexError:
            return None
    def getObjectRow(self, row):
        try:
            return self.objects[row]
        except IndexError:
            return None
    def getProperty(self, index):
        if not index.isValid():
            return None
        propertyIndex = index.column()
        try:
            return self.properties[propertyIndex]
        except IndexError:
            return None

    def rowCount(self, parent=None, *args, **kwargs):
        return len(self.objects)

    def columnCount(self, parent=None, *args, **kwargs):
        return len(self.properties)

    def data(self, index, role=Qt.DisplayRole):
        if not index.isValid(): 
            return None
        # elif role != Qt.DisplayRole: 
        #     return None
        obj = self.getObject(index)
        prop = self.getProperty(index)
        if (obj is None) or (prop is None):
            return None
        try:
            if role in [Qt.DisplayRole, Qt.EditRole]:
                return getAttrRecursive(obj, prop['attr'])
        except:
            return None
        return None

    def setData(self, index, value, role=Qt.EditRole):
        if not index.isValid():
            logging.info("index invalid")
            return False
        obj = self.getObject(index)
        prop = self.getProperty(index)
        if (obj is None) or (prop is None):
            logging.info("obj is None or prop is None")
            return None
        try:
            if role == Qt.EditRole:
                logging.info(type(value))
                logging.info([obj,prop['attr'],value])
                setAttrRecursive(obj, prop['attr'], value)
                return True
        except:
            traceback.print_exc()
            return False
        return False

    def flags(self, index):
        flags = QAbstractTableModel.flags(self, index)
        if not index.isValid():
            return flags
        prop = self.getProperty(index)
        if prop is None:
            return flags
        flags |= Qt.ItemIsEnabled
        flags |= Qt.ItemIsSelectable
        mode = prop.get('mode', "Read/Write")
        if "Write" in mode:
            flags |= Qt.ItemIsEditable
        return flags
    def headerData(self, col, orientation, role= Qt.DisplayRole):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            if col<len(self.properties):
                p=self.properties[col]
                if p!=None:
                    return QtCore.QVariant(p["header"])
                else:
                    return QtCore.QVariant()
            # return QtCore.QVariant(self.properties[col]['header'])
        return QtCore.QVariant()

    def setObjects(self,objects,properties):
        self.beginResetModel()
        self.objects=objects
        self.properties=properties
        self.endResetModel()
    def setObjectsOnly(self,objects):
        self.beginResetModel()
        self.objects=objects
        self.endResetModel()
    def appendObjects(self, objs):
        i=len(self.objects)
        num=len(objs)
        self.beginInsertRows(QModelIndex(), i, i + num - 1)
        at=0
        for objectIndex in range(i, i + num):
            self.objects.insert(objectIndex, objs[at])
            at+=1
        self.endInsertRows()
        return True
    def getRowNum(self,obj):
        at=0
        for one in self.objects:
            if one==obj:
                return at
            at+=1
        return -1
    def rmObjects(self, objs):
        for obj in objs:
            i=self.getRowNum(obj)
            if i!=-1:
                num=1
                self.beginRemoveRows(QModelIndex(), i, i + num - 1)
                del self.objects[i:i+num]
                self.endRemoveRows()
        return True
    def insertObjects(self, i, num=1):
        if (len(self.objects) == 0 ) or (num <= 0):
            return False
        i = min([max([0, i]), len(self.objects)])  # Clamp i to within [0, # of objects].
        self.beginInsertRows(QModelIndex(), i, i + num - 1)
        for objectIndex in range(i, i + num):
            if len(self.objects):
                copyIndex = min([max([0, objectIndex]), len(self.objects) - 1])  # Clamp objectIndex to a valid object index.
                self.objects.insert(objectIndex, copy.deepcopy(self.objects[copyIndex]))
        self.endInsertRows()
        return True

    def removeObjects(self, i, num=1):
        if (len(self.objects) == 0) or (num <= 0):
            return False
        i = min([max([0, i]), len(self.objects) - 1])  # Clamp i to a valid object index.
        num = min([num, len(self.objects) - i])  # Clamp num to a valid number of objects.
        self.beginRemoveRows(QModelIndex(), i, i + num - 1)
        del self.objects[i:i+num]
        self.endRemoveRows()
        return True

    def moveObjects(self, indices, moveToIndex):
        if len(self.objects) <= 1:
            return False
        try:
            if type(indices) is not list:
                indices = list(indices)
            for i, idx in enumerate(indices):
                indices[i] = min([max([0, idx]), len(self.objects) - 1])  # Clamp indices to valid object indices.
            moveToIndex = min([max([0, moveToIndex]), len(self.objects) - 1])  # Clamp moveToIndex to a valid object index.
            self.beginResetModel()
            objectsToMove = []
            for i in indices:
                objectsToMove.append(self.objects[i])
            for i in reversed(indices):
                del self.objects[i]
            for i, obj in enumerate(objectsToMove):
                j = moveToIndex + i
                j = min([max([0, j]), len(self.objects)])  # Clamp j to within [0, # of objects].
                self.objects.insert(j, obj)
            self.endResetModel()
            return True
        except:
            return False
    def clearObjects(self):
        if len(self.objects):
            self.beginResetModel()
            del self.objects[:]
            self.endResetModel()

    def sort(self, Ncol, order):
        """Sort table by given column number.
        """
        self.layoutAboutToBeChanged.emit()#SIGNAL("layoutAboutToBeChanged()"))
        print(Ncol)
        # self.objects =sorted(self.objects, key=operator.itemgetter(Ncol))        
        try:
            self.objects =sorted(self.objects,key=lambda x: getattr(x,self.properties[Ncol]["attr"]))
        except TypeError:
            pass
        if order == Qt.DescendingOrder:
            self.objects.reverse()
        self.layoutChanged.emit()#SIGNAL("layoutChanged()"))

class ObjectView(MyTableView):
    def __init__(self, model, parent=None):
        super().__init__(parent)
    
    def setModel(self, model):
        QTableView.setModel(self, model)
        for i, prop in enumerate(model.properties):
            # logging.info([i,prop,prop.get('visible',True)])
            if prop.get('hide',False)==True:
                self.setColumnHidden(i,True)
            else:
                if prop.get('visible',True)==True:
                    self.setColumnHidden(i,False)
                else:
                    self.setColumnHidden(i,True)
            width=prop.get('width',100)
            self.setColumnWidth(i,width)
            # self.setItemDelegateForColumn(i, prop.get('delegate'))

    def selectedRows(self):
        selectedIndexes = self.selectionModel().selection().indexes()
        rows = set()
        for index in selectedIndexes:
            rows.add(index.row())
        return sorted(list(rows))

    def selectedColumns(self):
        selectedIndexes = self.selectionModel().selection().indexes()
        columns = set()
        for index in selectedIndexes:
            columns.add(index.column())
        return sorted(list(columns))

    def clearObjects(self):
        self.model().clearObjects()



if __name__ == "__main__":
    import sys
    from PyQt5.QtWidgets import QApplication

    # We'll create a table model/view for a list of these objects.
    class MyObject(object):
        def __init__(self, name="New Obj", s="", i=0, f=0.0, b=True, hasChild=True):
            self.name = name
            self.strValue = s
            self.intValue = i
            self.floatValue = f
            self.boolValue = b
            self.dateValue = datetime.now()
            self._fileName = ""
            if hasChild:
                self.child = MyObject(name, s, i, f, b, False)

        # We'll have the model/view access the fileName property
        # rather than the _fileName attribute so that we
        # run our custom code whenever the fileName is changed.
        @property
        def fileName(self):
            return self._fileName

        @fileName.setter
        def fileName(self, fileName):
            if len(fileName) and (fileName != self._fileName):
                print("Setting file name for " +
                      self.name + " to " + fileName + ".")
                self._fileName = fileName

        # We'll have the model/view call this
        # when a button is clicked in the object's row/column.
        def clicked(self):
            print(self.name + " was clicked.")

    # Create the QApplication.
    app = QApplication(sys.argv)

    # Create our object list.
    a = MyObject("obj A", "a str", 3, 0.042, True)
    b = MyObject("obj B", "b str", -1, -10.069, False)
    objects = [a, b]

    # Specify the properties to display in the model/view.
    properties = [
        {'width':90,'visible':True,'attr': "name",           'header': "Read Only Name",      'mode': "Read Only"},
        {'attr': "strValue",       'header': "String"},
        {'attr': "intValue",       'header': "Integer"},
        {'attr': "floatValue",     'header': "Float"},
        {'attr': "boolValue",      'header': "Bool"},
        {'attr': "dateValue",      'header': "Date/Time",           'text': "%c"},
        {'attr': "fileName",       'header': "File Name",
            'action': "fileDialog"},
        {'attr': "clicked",        'header': "Button",
            'action': "button", 'text': "Click Me!"},
        {'attr': "child.intValue", 'header': "Child Int"},
        {'attr': "strValue",       'header': "String Combo Box",
            'choices': ['First Choice', 'Second Choice']},
        {'attr': "child.intValue",
            'header': "Child Int Combo Box", 'choices': [42, 82]},
        {'attr': "floatValue",     'header': "Float Combo Box",     'choices': [('PI', 3.14), ('-PI', -3.14)]}]

    # Print property names/values/types prior to editing.
    print("---------- BEFORE EDITING ----------")
    for obj in objects:
        for prop in properties:
            attr = prop['attr']
            try:
                value = getattr(obj, attr)
                print(attr, value, type(value))
            except:
                pass

    # Create the model/view.
    model = ObjectModel()
    w=QtWidgets.QWidget()

    view = ObjectView(model)

    model.setObjects(objects,properties)
    view.setModel(model)
    # Show the model/view and run the application.
    # view.setAttribute(Qt.WA_DeleteOnClose)
    # view.show()
    v=QtWidgets.QVBoxLayout(w)
    b0=QtWidgets.QPushButton("change object")
    def b0_click():
        objects[0].name="000000000"
        # model.getObjectRow(0).name="0000000"
        view.setFocus()
    b0.clicked.connect(b0_click)
    v.addWidget(b0)

    b=QtWidgets.QPushButton("add")
    def b_click():
        c = MyObject("obj A", "a str", 3, 0.042, True)
        model.appendObjects([c])
        pass
    b.clicked.connect(b_click)
    v.addWidget(b)

    b2=QtWidgets.QPushButton("rm first")
    def b2_click():
        c = model.getObjectRow(0)
        model.rmObjects([c])
        pass
    b2.clicked.connect(b2_click)
    v.addWidget(b2)

    b3=QtWidgets.QPushButton("rm select")
    def b3_click():
        selections = view.selectionModel()
        list1 = selections.selectedIndexes()
        objs=[]
        for i in range(len(list1)):
            current = list1[i]
            obj =model.getObject(current)
            if obj in objs:
                pass
            else:
                objs.append(obj)
        model.rmObjects(objs)
    b3.clicked.connect(b3_click)
    v.addWidget(b3)

    v.addWidget(view)
    w.show()
    status = app.exec()

    # Print property names/values/types post editing.
    print("---------- AFTER EDITING ----------")
    for obj in objects:
        for prop in properties:
            attr = prop['attr']
            try:
                value = getattr(obj, attr)
                print(attr, value, type(value))
            except:
                pass
    sys.exit(status)
